package com.yuqih.common.servlet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;

import com.yuqih.common.spring.StaticContextContainer;

public class RequestContextFilter implements Filter {

	private String contextPath;
	private String contextPathWithoutStart;
	private RequestContextFilterConfig config;
	private RequestContext requestContext;

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		contextPath = filterConfig.getServletContext().getContextPath();
		contextPathWithoutStart = StringUtils.removeStart(contextPath, "/");
	}

	private RequestContextFilterConfig getConfig() {
		if (config == null) {
			config = StaticContextContainer.getContext().getBean(RequestContextFilterConfig.class);
		}
		return config;
	}

	private RequestContext getRequestContext() {
		if (requestContext == null) {
			requestContext = StaticContextContainer.getContext().getBean(RequestContext.class);
		}
		return requestContext;
	}

	private String getUri(HttpServletRequest httpRequest) throws UnsupportedEncodingException {
		String uri = StringUtils.removeStart(httpRequest.getRequestURI(), contextPath);
		if (StringUtils.startsWith(uri, "/")) {
			uri = StringUtils.removeStart(uri, "/");
		}
		if (httpRequest.getCharacterEncoding() == null) {
			uri = URLDecoder.decode(uri, StandardCharsets.UTF_8.name());
		} else {
			uri = URLDecoder.decode(uri, httpRequest.getCharacterEncoding());
		}
		return uri;
	}

	private void buildRequestContext(HttpServletRequest httpRequest, String uri, RequestContext requestContext)
			throws UnsupportedEncodingException {
		requestContext.setServerName(httpRequest.getServerName());
		requestContext.setContextPath(contextPathWithoutStart);

		requestContext.setUri(uri);

		requestContext.setMethod(httpRequest.getMethod());
		requestContext.setRemoteIp(RequestUtil.getRemoteIp(httpRequest, getConfig().getRemoteIpHeaderNames()));
		requestContext.setParams(new LinkedHashMap<String, String[]>( httpRequest.getParameterMap()));

		Map<String, String[]> headers = new HashMap<String, String[]>();
		Enumeration<String> headerNames = httpRequest.getHeaderNames();
		String headerName;
		Enumeration<String> headerContents;
		List<String> headerContentList;
		while (headerNames != null && headerNames.hasMoreElements()) {
			headerName = headerNames.nextElement();
			headerContents = httpRequest.getHeaders(headerName);
			headerContentList = new ArrayList<String>();
			while (headerContents != null && headerContents.hasMoreElements()) {
				headerContentList.add(headerContents.nextElement());
			}
			headers.put(headerName, headerContentList.toArray(new String[headerContentList.size()]));
		}
		requestContext.setHeaders(headers);
	}

	private boolean isExcluded(String uri) {
		if (getConfig().getExcludeUris() != null) {
			for (String prefix : getConfig().getExcludeUris()) {
				if (StringUtils.equals(uri, prefix)) {
					return true;
				}
				if (StringUtils.endsWith(prefix, "/") && StringUtils.startsWith(uri, prefix)) {
					return true;
				}
			}
		}

		return false;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		
		
		httpRequest.getSession();

		String uri = getUri(httpRequest);
		if (isExcluded(getRequestContext().getUri())) {
			chain.doFilter(request, response);
			return;
		}

		CustomHttpServletRequest requestWrapper = new CustomHttpServletRequest(httpRequest);
		RequestContext requestContext = getRequestContext();
		buildRequestContext(requestWrapper, uri, requestContext);

		try {
			chain.doFilter(requestWrapper, response);
		} catch (Exception e) {
			e.printStackTrace();
			if (requestContext.getError() == null) {
				requestContext.setError(e);
			}
		} finally {
			requestContext.setResponseTime(System.currentTimeMillis());
			requestContext.setResponseStatus(httpResponse.getStatus());

			log(requestWrapper, requestContext);
		}

	}

	private void log(CustomHttpServletRequest requestWrapper, RequestContext requestContext) {
		try {
			final RequestContext toLog = RequestContext.newInstance(requestContext);

			if (requestWrapper.isApplicationJson && requestWrapper.inputStream == null) {
				IOUtils.copy(requestWrapper.getInputStream(), new NullOutputStream());
			}

			if (requestWrapper.inputStream != null && requestWrapper.inputStream instanceof InputStreamWrapper) {
				InputStreamWrapper inputStreamWrapper = (InputStreamWrapper) requestWrapper.inputStream;
				toLog.setRequestBody(inputStreamWrapper.baos.toString(StandardCharsets.UTF_8.name()));
			}

			getConfig().getExecutor().submit(new Callable<Void>() {

				@Override
				public Void call() throws Exception {
					getConfig().getLogger().log(toLog);
					return null;
				}
			});
		} catch (Exception ignore) {

		}
	}

	@Override
	public void destroy() {

	}

	private static class CustomHttpServletRequest extends HttpServletRequestWrapper {

		private static final MediaType[] applicationJsonTypes = { MediaType.APPLICATION_JSON,
				new MediaType("application", "*+json") };

		private boolean isApplicationJson;
		private ServletInputStream inputStream;

		public CustomHttpServletRequest(HttpServletRequest request) {
			super(request);
			String contentType = getRequest().getContentType();
			if (StringUtils.isNotBlank(contentType)) {
				MediaType type = MediaType.parseMediaType(contentType);
				for (MediaType applicationJsonType : applicationJsonTypes) {
					if (applicationJsonType.includes(type)) {
						isApplicationJson = true;
						break;
					}
				}
			}
		}

		@Override
		public ServletInputStream getInputStream() throws IOException {

			if (inputStream != null) {
				return inputStream;
			}

			ServletInputStream in = super.getRequest().getInputStream();

			if (isApplicationJson) {
				this.inputStream = new InputStreamWrapper(in, new ByteArrayOutputStream());
			} else {
				this.inputStream = in;
			}
			return inputStream;
		}
	}

	private static class InputStreamWrapper extends ServletInputStream {

		private volatile ServletInputStream in;
		private final ByteArrayOutputStream baos;

		/**
		 * @param out
		 * @param baos
		 */
		InputStreamWrapper(ServletInputStream in, ByteArrayOutputStream baos) {
			this.in = in;
			this.baos = baos;
		}

		@Override
		public int read() throws IOException {
			int b = in.read();
			if (b != -1) {
				baos.write(b);
			}
			return b;
		}

		@Override
		public int read(byte b[], int off, int len) throws IOException {
			int c = in.read(b, off, len);
			if (c > 0) {
				baos.write(b, off, c);
			}
			return c;
		}

		@Override
		public int read(byte b[]) throws IOException {
			return read(b, 0, b.length);
		}

		@Override
		public long skip(long n) throws IOException {
			return in.skip(n);
		}

		@Override
		public int available() throws IOException {
			return in.available();
		}

		@Override
		public void close() throws IOException {
			in.close();
		}

		@Override
		public synchronized void mark(int readlimit) {
			in.mark(readlimit);
		}

		@Override
		public synchronized void reset() throws IOException {
			in.reset();
		}

		@Override
		public boolean markSupported() {
			return in.markSupported();
		}

		@Override
		public boolean isFinished() {
			return in.isFinished();
		}

		@Override
		public boolean isReady() {
			return in.isReady();
		}

		@Override
		public void setReadListener(ReadListener listener) {
			in.setReadListener(listener);
		}

	}

}
